# -*- bazel -*-

load("@drake//tools/install:install.bzl", "install", "install_files")
load("@drake//tools/lint:python_lint.bzl", "python_lint")
load("@drake//tools/skylark:cc.bzl", "cc_binary", "cc_library")
load("@drake//tools/skylark:drake_java.bzl", "drake_java_binary")
load("@drake//tools/skylark:java.bzl", "java_library")
load("@drake//tools/skylark:py.bzl", "py_library")
load(
    "@drake//tools/workspace:generate_export_header.bzl",
    "generate_export_header",
)
load("@drake//tools/workspace:generate_file.bzl", "generate_file")

licenses([
    "notice",  # BSD-3-Clause
    "restricted",  # LGPL-2.1 AND LGPL-2.1+
])

package(default_visibility = ["//visibility:public"])

# Provide an opt-out that avoids installing Java LCM libraries (e.g., we don't
# need Java when we're building a Python wheel). The java targets will still be
# defined, they just won't be part of //:install.
config_setting(
    name = "lcm_install_java_is_off",
    values = {"define": "LCM_INSTALL_JAVA=OFF"},
)

config_setting(
    name = "linux",
    constraint_values = ["@platforms//os:linux"],
    visibility = ["//visibility:private"],
)

# Generate header to provide ABI export symbols for LCM.
generate_export_header(
    out = "lcm/lcm_export.h",
    lib = "lcm",
    static_define = "LCM_STATIC",
)

# These options are used when building all LCM C/C++ code.
# They do not flow downstream to LCM-using libraries.
LCM_COPTS = [
    "-w",
    "-std=gnu11",
    "-fvisibility=hidden",
]

LCM_PUBLIC_HEADERS = [
    # Public (installed) headers
    "lcm/eventlog.h",
    "lcm/lcm.h",
    "lcm/lcm-cpp.hpp",
    "lcm/lcm-cpp-impl.hpp",
    "lcm/lcm_coretypes.h",
    "lcm/lcm_export.h",  # N.B. This is from generate_export_header above.
    "lcm/lcm_version.h",
]

# The paths within the LCM source archive that make #include statements work.
LCM_PUBLIC_HEADER_INCLUDES = [
    ".",
    # TODO(jwnimmer-tri): The 'lcm' is needed so we can generate lcm_export.h
    # with the correct path and still include it like '#include "lcm_export.h"'
    # from other LCM headers. However, this "pollutes" the include paths of
    # everyone using LCM. Can we do better?
    "lcm",
]

# We only build a shared-library flavor of liblcm.so, because its LGPL-licensed
# and thus should never be linked statically.  Building C++ shared libraries
# in Bazel is not well-supported, but for C code is relatively trivial.  This
# rule creates the runtime artifact -- a loadable shared library with its deps
# linked dynamically.  The `name = "lcm"` rule below provides the compile-time
# artifact that combines the shared library with its headers.
cc_binary(
    name = "libdrake_lcm.so",
    srcs = [
        # Sources
        "lcm/eventlog.c",
        "lcm/lcm.c",
        "lcm/lcm_file.c",
        "lcm/lcm_memq.c",
        "lcm/lcm_mpudpm.c",
        "lcm/lcm_tcpq.c",
        "lcm/lcm_udpm.c",
        "lcm/lcmtypes/channel_port_map_update_t.c",
        "lcm/lcmtypes/channel_to_port_t.c",
        "lcm/ringbuffer.c",
        "lcm/udpm_util.c",
        # Private headers
        "lcm/dbg.h",
        "lcm/ioutils.h",
        "lcm/lcm_internal.h",
        "lcm/lcmtypes/channel_port_map_update_t.h",
        "lcm/lcmtypes/channel_to_port_t.h",
        "lcm/ringbuffer.h",
        "lcm/udpm_util.h",
    ] + LCM_PUBLIC_HEADERS,
    copts = LCM_COPTS,
    includes = LCM_PUBLIC_HEADER_INCLUDES,
    linkopts = select({
        ":linux": [
            "-pthread",  # For lcm_(mp)udpm.c.
            "-Wl,-soname,libdrake_lcm.so",
        ],
        "//conditions:default": [],
    }),
    linkshared = 1,
    visibility = ["//visibility:private"],
    deps = ["@glib"],
)

# This is the compile-time target for liblcm.  See above for details.
cc_library(
    name = "lcm",
    srcs = ["libdrake_lcm.so"],
    hdrs = LCM_PUBLIC_HEADERS,
    includes = LCM_PUBLIC_HEADER_INCLUDES,
)

# This is the header-only library used by generated messages.
cc_library(
    name = "lcm_coretypes",
    hdrs = ["lcm/lcm_coretypes.h"],
    includes = ["."],
    linkstatic = True,
)

cc_binary(
    name = "lcm-logger",
    srcs = [
        "lcm-logger/glib_util.c",
        "lcm-logger/glib_util.h",
        "lcm-logger/lcm_logger.c",
    ],
    copts = LCM_COPTS,
    deps = [
        ":lcm",
        "@glib",
    ],
)

cc_binary(
    name = "lcm-logplayer",
    srcs = [
        "lcm-logger/lcm_logplayer.c",
    ],
    copts = LCM_COPTS,
    deps = [":lcm"],
)

cc_binary(
    name = "lcm-gen",
    srcs = [
        "lcm/lcm_version.h",
        "lcmgen/emit_c.c",
        "lcmgen/emit_cpp.c",
        "lcmgen/emit_csharp.c",
        "lcmgen/emit_go.c",
        "lcmgen/emit_java.c",
        "lcmgen/emit_lua.c",
        "lcmgen/emit_python.c",
        "lcmgen/getopt.c",
        "lcmgen/getopt.h",
        "lcmgen/lcmgen.c",
        "lcmgen/lcmgen.h",
        "lcmgen/main.c",
        "lcmgen/tokenize.c",
        "lcmgen/tokenize.h",
    ],
    copts = LCM_COPTS,
    includes = ["."],
    deps = [
        "@glib",
    ],
)

cc_binary(
    name = "_lcm.so",
    srcs = [
        "lcm-python/module.c",
        "lcm-python/pyeventlog.c",
        "lcm-python/pylcm.c",
        "lcm-python/pylcm.h",
        "lcm-python/pylcm_subscription.c",
        "lcm-python/pylcm_subscription.h",
        "lcm/dbg.h",
    ],
    copts = LCM_COPTS,
    linkshared = 1,
    linkstatic = 1,
    visibility = ["//visibility:private"],
    deps = [
        ":lcm",
        "@drake//tools/workspace/python:cc_headers",
        "@drake//tools/workspace/python:cc_libs",
    ],
)

# Downstream users of lcm-python expect to say "import lcm". However, in the
# sandbox the python package is located at lcm-python/lcm/__init__.py to match
# the source tree structure of LCM; without declaring an `imports = [...]` path
# the import would fail.
#
# Normally we'd add `imports = ["lcm-python"]` to establish a PYTHONPATH at the
# correct subdirectory, and that almost works -- except that the native code's
# RUNPATH entries are not quite correct. Even though `./_lcm.so` (the glue)
# resolves correctly in the sandbox, it needs to then load the main library
# `liblcm.so` to operate. That happens via its RUNPATH, but because the RUNPATH
# is relative, when the lcm module is loaded from the wrong sys.path entry, the
# RUNPATH no longer works.
#
# To repair this, we'll generate our own init file that pre-loads the shared
# library (using python ctypes with the realpath to the shared library) before
# calling the upstream __init__.
#
# Note that this generated __init__.py shim is neither used nor present in the
# installed copy of Drake; once Drake is installed, the paths are standard and
# there is no aliasing confusion.
generate_file(
    name = "gen/lcm/__init__.py",
    content = """
import ctypes
from pathlib import Path
# The base_dir refers to the base of our package.BUILD.bazel.
_base_dir = Path(__path__[0]).resolve().parent.parent
# Load the native code.
ctypes.cdll.LoadLibrary(_base_dir / '_lcm.so')
# We need to tweak the upstream __init__ before we run it.
_filename = _base_dir / 'lcm-python/lcm/__init__.py'
_text = _filename.read_text(encoding='utf-8')
# Respell where the native code comes from.
_text = _text.replace('from lcm import _lcm', 'import _lcm')
_text = _text.replace('from lcm._lcm import', 'from _lcm import')
exec(compile(_text, _filename, 'exec'))
""",
    visibility = ["//visibility:private"],
)

py_library(
    name = "lcm-python-upstream",
    srcs = ["lcm-python/lcm/__init__.py"],  # Actual code from upstream.
    data = [":_lcm.so"],
    visibility = ["//visibility:private"],
)

py_library(
    name = "lcm-python",
    srcs = ["gen/lcm/__init__.py"],  # Shim, from the genrule above.
    imports = ["gen"],
    deps = [":lcm-python-upstream"],
)

java_library(
    name = "lcm-java",
    srcs = glob(["lcm-java/lcm/**/*.java"]),
    javacopts = [
        # Suppressed until lcm-proj/lcm#159 is fixed.
        "-XepDisableAllChecks",
    ],
    runtime_deps = [
        "@com_jidesoft_jide_oss//jar",
        "@commons_io//jar",
        "@org_apache_xmlgraphics_commons//jar",
    ],
    deps = ["@net_sf_jchart2d//jar"],
)

drake_java_binary(
    name = "lcm-logplayer-gui",
    main_class = "lcm.logging.LogPlayer",
    runtime_deps = [":lcm-java"],
)

drake_java_binary(
    name = "lcm-spy",
    jvm_flags = [
        # These flags are copied from the lcm/lcm-java/lcm-spy.sh.in.
        "-Djava.net.preferIPv4Stack=true",
        "-ea",
    ],
    main_class = "lcm.spy.Spy",
    runtime_deps = [
        ":lcm-java",
    ],
)

CMAKE_PACKAGE = "lcm"

install_files(
    name = "install_cmake_config",
    dest = "lib/cmake/{}".format(CMAKE_PACKAGE),
    files = [
        "lcm-cmake/lcmUtilities.cmake",
        "@drake//tools/workspace/lcm:{}-config.cmake".format(CMAKE_PACKAGE),  # noqa
        "@drake//tools/workspace/lcm:{}-config-version.cmake".format(CMAKE_PACKAGE),  # noqa
    ],
    strip_prefix = ["**/"],
    allowed_externals = ["@drake//:.bazelproject"],
    visibility = ["//visibility:private"],
)

install(
    name = "install_python",
    targets = [
        ":_lcm.so",
        ":lcm-python-upstream",
    ],
    library_dest = "lib/python@PYTHON_VERSION@/site-packages/lcm",
    py_strip_prefix = ["lcm-python"],
)

# Drake's install of LCM alongside Drake (both as part of our CMake install,
# and in our binary packages) is deprecated and will be removed on 2025-05-01.
#
# If you still need LCM tools in your own project, add LCM as a CMake external
# to your project.
#
# On 2025-05-01 we will remove everything from this target except for the
# C++ runtime shared library and its license.
install(
    name = "install",
    install_tests = [
        "@drake//tools/workspace/lcm:test/lcm-gen_install_test.py",
    ],
    targets = [
        ":lcm-gen",
        ":lcm-logger",
        ":lcm-logplayer",
        ":libdrake_lcm.so",
    ] + select({
        ":lcm_install_java_is_off": [],
        "//conditions:default": [
            ":lcm-java",
            ":lcm-logplayer-gui",
            ":lcm-logplayer-gui-launcher",
            ":lcm-spy",
            ":lcm-spy-launcher",
        ],
    }),
    py_strip_prefix = ["lcm-python"],
    hdrs = LCM_PUBLIC_HEADERS,
    hdr_dest = "include/" + CMAKE_PACKAGE,
    docs = [
        "AUTHORS",
        "COPYING",
        "NEWS.md",
    ],
    allowed_externals = [
        "@com_jidesoft_jide_oss//jar",
        "@commons_io//jar",
        "@net_sf_jchart2d//jar",
        "@org_apache_xmlgraphics_commons//jar",
    ],
    rename = {
        "share/java/liblcm-java.jar": "lcm.jar",
        "bin/lcm-logplayer-gui-launcher.sh": "lcm-logplayer-gui",
        "bin/lcm-spy-launcher.sh": "lcm-spy",
    },
    deps = [
        ":install_cmake_config",
        ":install_python",
    ],
)

python_lint(extra_srcs = [":test/lcm-gen_install_test.py"])
